<?php
/**
 * 火币平台接口类
 * Created by PhpStorm.
 * User: zengdeshun
 * Date: 2018/5/12
 * Time: 14:10
 */

namespace platform\driver\huobi;

use think\Log;
use think\Cache;
use Workerman\Connection\AsyncTcpConnection;
use platform\driver\Driver;
use exception\Platform as PlatformException;
use exception\Huobi as HuobiException;

class Huobi extends Driver
{
    /**
     * rest api接口地址
     * @var string
     */
    private $api = '';

    /**
     * webSocket接口地址
     * @var string
     */
    private $stream = '';

    /**
     * 账户ID
     * @var string
     */
    private $accountId = '';

    /**
     * API 访问密钥
     * @var string
     */
    private $accessKey = '';

    /**
     * 签名密钥
     * @var string
     */
    private $secretKey = '';

    /**
     * api请求路径
     * @var string
     */
    private $apiMethod = '';

    /**
     * api请求方法
     * @var string
     */
    private $reqMethod = '';

    public function __construct($param = [])
    {
        // date_default_timezone_set("Etc/GMT+0");
        $config = file_get_contents(dirname(__FILE__) . '/config.json');
        $config = json_decode($config, true);
        $this->api = $config['api'];
        $this->stream = $config['stream'];
        if (isset($param['access_key']) && isset($param['secret_key'])) {
            $this->accessKey = $param['access_key'];
            $this->secretKey = $param['secret_key'];
            $this->accountId = isset($param['account_code']) ? $param['account_code'] : '';
        } else {
            $this->accessKey = $config['access_key'];
            $this->secretKey = $config['secret_key'];
        }
    }

    /**
     * 火币下单接口
     * @param $data
     * @return array
     * @throws \Exception
     */
    public function buy(...$data)
    {
        list($symbol, $quantity, $price, $type) = $data;
        $this->apiMethod  = '/v1/order/orders/place';
        $this->reqMethod = 'POST';
        $param = [
            'account-id' => $this->accountId,
            'amount'     => $quantity,
            'source'     => 'api',
            'symbol'     => $symbol,
            'type'       => $type
        ];
        if ($price) {
            $param['price'] = $price;
        }
        $url = $this->createSignUrl();
        $res = curl($url, json_encode($param), $this->reqMethod, ["Content-Type: application/json"]);
        $res = json_decode($res, true);
        if(isset($res['err-code'])) {
            if($res['err-code'] == 'order-limitorder-price-max-error'){
                $patterns = '/(\d+)\.(\d+)/is';
                preg_match ($patterns,$res['err-msg'],$arr);
                if (empty($arr)){
                    $patterns ='/\d+/';
                    preg_match($patterns,$res['err-msg'],$arr);
                }
                exception(HuobiException::getErrorMsg(HuobiException::ORDER_LIMITORDER_PRICE_MAX_ERROR,$arr[0]), HuobiException::ORDER_LIMITORDER_PRICE_MAX_ERROR, 'exception\Huobi');
            }
            if($res['err-code'] == 'order-limitorder-amount-min-error' || $res['err-code'] == 'order-marketorder-amount-min-error'){
                $patterns = '/(\d+)\.(\d+)/is';
                preg_match($patterns,$res['err-msg'],$arr);
                if (empty($arr)){
                    $patterns ='/\d+/';
                    preg_match($patterns,$res['err-msg'],$arr);
                }
                exception(HuobiException::getErrorMsg(HuobiException::ORDER_LIMITORDER_AMOUNT_MIN_ERROR,$arr[0]), HuobiException::ORDER_LIMITORDER_AMOUNT_MIN_ERROR, 'exception\Huobi');
            }
            if (array_key_exists($res['err-code'], $this->_errorCode)) {
                $code = str_replace('-','_',$this->_errorCode[$res['err-code']]);
                exception(HuobiException::getErrorMsg($code),  $code, 'exception\Huobi');
                Log::record('Huobi失败，错误码' . $this->_errorCode['code'] . '错误信息' . $res['err-code']);
            }
        }
        if ($res['status'] != 'ok') {
            exception(PlatformException::getErrorMsg(PlatformException::E_ORDER_ERROR) . ' ' . $res['err-msg'], PlatformException::E_ORDER_ERROR, 'exception\Platform');
        }
        return $res['data'];
    }

    /**
     * 申请撤销一个订单请求
     * @param $orderId
     * @return array
     * @throws \Exception
     */
    public function cancel(...$data)
    {
        list($orderId) = $data;
        $this->apiMethod  = '/v1/order/orders/' . $orderId . '/submitcancel';
        $this->reqMethod = 'POST';
        $url = $this->createSignUrl();
        $res = curl($url, [], $this->reqMethod, ["Content-Type: application/json"]);
        $res = json_decode($res,true);

        if(isset($res['err-code'])) {
            if (array_key_exists($res['err-code'], $this->_errorCode)) {
                $code = str_replace('-','_',$this->_errorCode[$res['err-code']]);
                exception(HuobiException::getErrorMsg($code),  $code, 'exception\Huobi');
                Log::record('huobi下单失败，错误码' . $this->_errorCode['code'] . '错误信息' . $res['err-code']);
            }
        }
        return $res;
    }

    /**
     * 查询某个订单详情
     * @param $orderId
     * @return array
     * @throws \Exception
     */
    public function order(...$data)
    {
        list($orderId) = $data;
        $this->apiMethod  = '/v1/order/orders/' . $orderId;
        $this->reqMethod = 'GET';
        $url = $this->createSignUrl();
        $res = curl($url, '', $this->reqMethod, ["Content-Type: application/json"]);
        $res = json_decode($res, true);
        if ($res['status'] != 'ok') {
            exception(PlatformException::getErrorMsg(PlatformException::E_GET_ORDER_ERROR), PlatformException::E_GET_ORDER_ERROR, 'exception\Platform');
        }
        return $res['data'];
    }

    /**
     * 查询Pro站支持的所有交易对及精度
     * @return mixed
     * @throws \Exception
     */
    public function symbols()
    {
        $this->apiMethod = '/v1/common/symbols';
        $this->reqMethod = 'GET';
        $url = $this->createSignUrl([]);
        $data  = curl($url, '', $this->reqMethod, ['Content-Type: application/json']);
        return $data;
    }

    /**
     * 查询Pro站指定账户的余额
     * @return mixed
     * @throws \Exception
     */
    public function balance()
    {
        $this->apiMethod = "/v1/account/accounts/{$this->accountId}/balance";
        $this->reqMethod = 'GET';
        $url = $this->createSignUrl([]);
        $data  = curl($url, '', $this->reqMethod, ['Content-Type: application/json']);
        $data = json_decode($data, true);
        if(isset($data['err-code'])) {
            if (array_key_exists($data['err-code'], $this->_errorCode)) {
                $code = str_replace('-', '_', $this->_errorCode[$data['err-code']]);
                exception(HuobiException::getErrorMsg($code), $code, 'exception\Huobi');
                Log::record('huobi失败，错误码' . $this->_errorCode['code'] . '错误信息' . $data['err-code']);

            }
        }
        return $data;
    }

    /**
     * 获取用户所有账户接口
     * @return mixed
     * @throws \Exception
     */
    public function accounts ()
    {
        $this->apiMethod = '/v1/account/accounts';
        $this->reqMethod = 'GET';
        $url = $this->createSignUrl([]);
        try {
            $res  = curl($url, '', $this->reqMethod, ['Content-Type: application/json']);
            $data = json_decode($res, true);
            if ($data['status'] != 'ok') {
                exception(PlatformException::getErrorMsg(PlatformException::E_GET_ACCOUNT_ERROR), PlatformException::E_GET_ACCOUNT_ERROR, 'exception\Platform');
            }
        } catch (\Exception $e) {
            exception(PlatformException::getErrorMsg(PlatformException::E_GET_ACCOUNT_ERROR), PlatformException::E_GET_ACCOUNT_ERROR, 'exception\Platform');
        }
        return $data['data'];
    }

    /**
     * 市场深度
     */
    public function depth(array $symbols, callable $callback)
    {
        $param = [];
        foreach ($symbols as $symbol) {
            $param[] = [
                'sub' => "market.{$symbol}.depth.step0",// 不合并深度
                'id' => 'depth_' . utc_time()
            ];
        }

        $this->webSocketClient($param, $callback);
        return true;
    }

    /**
     * webSocket订阅交易数据
     * @param $symbol
     * @param $callback
     * @return bool
     */
    public function detailWebSocket($symbols, $callback)
    {
        $param = [];
        foreach ($symbols as $symbol) {
            $param[] = [
                'sub' => "market.{$symbol}.detail",
                'id' => 'detail_' . utc_time()
            ];
        }
        $this->webSocketClient($param, $callback);
    }

    /**
     * 获取K线数据
     * @param array $data
     * @return array
     */
    public function kline(array $data)
    {
        $path  = "/market/history/kline";
        $param = [
            'symbol' => $data['symbol'],
            'period' => $data['period']
        ];
        if ($data['size']) {
            $param['size'] = $data['size'];
        }
        $url = $this->createSignUrl($param, $path);
        try {
            $data = curl($url, '', 'GET', ["Content-Type: application/json"]);
        } catch (\Exception $e) {
            Log::record('火币请求k线数据出错' . $e->getMessage());
        }
        return $data;
    }

    /**
     * 生成验签URL
     * @param array $appendParam
     * @return string
     */
    private function createSignUrl($appendParam = [])
    {
        // 验签参数
        $param = [
            'AccessKeyId'      => $this->accessKey,
            'SignatureMethod'  => 'HmacSHA256',
            'SignatureVersion' => 2,
            'Timestamp'        => date('Y-m-d\TH:i:s', utc_time())
        ];
        if ($appendParam) {
            foreach($appendParam as $k=>$ap) {
                $param[$k] = $ap;
            }
        }
        $url = 'https://' . $this->api . $this->apiMethod . '?' . $this->bindParam($param);
        return $url;
    }

    /**
     * 组合参数
     * @param $param
     * @return string
     */
    private function bindParam($param)
    {
        $u = [];
        $sort_rank = [];
        foreach($param as $k=>$v) {
            $u[] = $k . '=' . urlencode($v);
            $sort_rank[] = ord($k);
        }
        asort($u);
        $u[] = 'Signature=' . urlencode($this->createSig($u));
        return implode('&', $u);
    }

    /**
     * 生成签名
     * @param $param
     * @return string
     */
    private function createSig($param)
    {
        $signParam = $this->reqMethod . "\n" . $this->api . "\n" . $this->apiMethod . "\n" . implode('&', $param);
        $signature = hash_hmac('sha256', $signParam, $this->secretKey, true);
        return base64_encode($signature);
    }

    /**
     * webSocket接口统一请求客户端
     * @param $param
     * @param $callback
     * @return bool
     */
    protected function webSocketClient($param, $callback)
    {
        try {
            $con = new AsyncTcpConnection($this->stream);
        } catch (\Exception $e) {
            Log::record('请求webSocket接口出错 ' . $this->stream . $e->getMessage());
        }

        $con->transport = 'ssl'; // 设置以ssl加密方式访问，使之成为wss
        $con->onConnect = function($con) use ($param) {
            if (!empty($param)) {
                if (is_array($param)) {
                    foreach ($param as $value) {
                        $con->send(json_encode($value));
                    }
                } else {
                    $con->send(json_encode($param));
                }
            }
        };

        $con->onMessage = function($con, $data) use ($callback) {
            $data = gzdecode($data);
            $data = json_decode($data, true);
            if (isset($data['ping'])) {
                $con->send(json_encode([
                    "pong" => $data['ping']
                ]));
            } else {
                call_user_func_array($callback, array($data));
            }
        };

        $con->connect();
        return true;
    }

    private $_errorCode =[
        'base-symbol-error'  =>  '29002',
        'base-currency-error'  =>  '29003',
        'base-date-error'  =>  '29004',
        'account-transfer-balance-insufficient-error'  =>  '29005',
        'bad-argument'  =>  '29006',
        'api-signature-not-valid'  =>  '29007',
        'gateway-internal-error'  =>  '29008',
        'security-require-assets-password'  =>  '29009',
        'audit-failed'  =>  '29010',
        'ad-ethereum-addresss'  =>  '29011',
        'order-accountbalance-error'  =>  '29012',
        'order-limitorder-price-error'  =>  '29013',
        'order-limitorder-amount-error'   =>  '29014',
        'order-orderprice-precision-error'   =>  '29015',
        'order-orderamount-precision-error'   =>  '29016',
        'order-marketorder-amount-error'   =>  '29017',
        'order-queryorder-invalid'   =>  '29018',
        'order-orderstate-error'   =>  '29019',
        'order-datelimit-error'   =>  '29020',
        'order-update-error'   =>  '29021',
        'validation-format-error' =>'29022',
    ];

    /**
     * REST方式获取行情
     * @return array
     */
    public function restTicker($symbol)
    {
        $data = [];
        try {
            $headers = [
                'Content-Type: application/json',
                'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36'
            ];

            $data = curl('https://' . $this->api . '/market/detail?symbol=' . $symbol, [], 'GET', $headers);
            $data = json_decode($data, true);
        } catch (\Exception $e) {
            Log::record('初始化火币请求行情数据出错' . $e->getMessage());
        }

        return $data;
    }

    /**
     * REST方式获取深度
     * @return array
     */
    public function restDepth($symbol)
    {
        $data = [];
        try {
            $headers = [
                'Content-Type: application/json',
                'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36'
            ];

            $data = curl('https://' . $this->api . '/market/depth?symbol=' . $symbol . '&type=step0', [], 'GET', $headers);
            $data = json_decode($data, true);
        } catch (\Exception $e) {
            Log::record('初始化火币请求深度数据出错' . $e->getMessage());
        }

        return $data;
    }
}